/*
 * theoraplayer.cpp
 *
 *  Created on: 7-ott-2008
 *      Author: fpistonesi
 */
/*

 Copyright (c) 2008-2009 Fabrizio Pistonesi

 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

 */

#include "CTheoraPlayer.h"

//#include "OHRTimer.h"
//
//OHRTimer gTimer;

//------------------------------------------------------------------------------
//! Ctor
//!
CTheoraPlayer::CTheoraPlayer(IrrlichtDevice* device) :
  iIrrDevice(device), iTexture(NULL),
  iVideoFile(NULL), iTheoraPacketsCount(0),
  iState(CTPS_IDLE), iImage(NULL)
{
  iIrrVideoDriver = device->getVideoDriver();
  iIrrLog = device->getLogger();
}

//------------------------------------------------------------------------------
//! Dtor
//!
CTheoraPlayer::~CTheoraPlayer()
{
  Stop();
}

//------------------------------------------------------------------------------
//! Create
//!
bool CTheoraPlayer::Create()
{
  // create blank texture
  core::dimension2di size(1,1);
  iBlankTexture = iIrrVideoDriver->addTexture(
    size, "CTheoraPlayerBlankTexture");

  u8* data = (u8*) iBlankTexture->lock();
  if (data != NULL)
  {
    memset(data, 0xFF, iBlankTexture->getPitch()*iBlankTexture->getSize().Height);
    iBlankTexture->unlock();
  }

  return true;
}

//------------------------------------------------------------------------------
//! Play
//!
bool CTheoraPlayer::Play(const c8* fileName, bool loop)
{
  // stop previous video
  Stop();

  iIsLooped = loop;

  // try to open file
  iVideoFile = iIrrDevice->getFileSystem()->createAndOpenFile(fileName);
  if (iVideoFile == NULL)
  {
    // failed to open file
    return false;
  }

  // initialize ogg decoder
  if (!PrepareOgg())
    return false;

  // prepare texture and buffer
  if (!PrepareBuffers())
    return false;

  iState = CTPS_PLAYING;

  return true;
}

//------------------------------------------------------------------------------
//! Stop
//!
bool CTheoraPlayer::Stop()
{
  if (iState == CTPS_IDLE)
    return true;

  if (iTheoraPacketsCount)
  {
    ogg_stream_clear(&iOggStreamState);
    theora_clear(&iTheoraDecoderState);
    theora_comment_clear(&iTheoraComment);
    theora_info_clear(&iTheoraInfo);
  }
  ogg_sync_clear(&iOggSyncState);

  if (iVideoFile != NULL) { iVideoFile->drop(); iVideoFile = NULL; }
  if (iTexture != NULL) { iTexture->drop(); iTexture = NULL; }
  if (iImage != NULL) { iImage->drop(); iImage = NULL; }

  iState = CTPS_IDLE;

  return true;
}

//------------------------------------------------------------------------------
//! PrepareOgg
//!
bool CTheoraPlayer::PrepareOgg()
{
  // set start pos
  iVideoFile->seek(0);

  /*
  Ok, Ogg parsing. The idea here is we have a bitstream
  that is made up of Ogg pages. The libogg sync layer will
  find them for us. There may be pages from several logical
  streams interleaved; we find the first theora stream and
  ignore any others.

  Then we pass the pages for our stream to the libogg stream
  layer which assembles our original set of packets out of
  them. It's the packets that libtheora actually knows how
  to handle.
  */

  int stateflag = 0;

  /* start up Ogg stream synchronization layer */
  ogg_sync_init(&iOggSyncState);

  /* init supporting Theora structures needed in header parsing */
  theora_comment_init(&iTheoraComment);
  theora_info_init(&iTheoraInfo);

  iTheoraPacketsCount = 0;
  iCurrFrame = 0;
  iCurrTime = 0;


  /* Ogg file open; parse the headers */

  /* Vorbis and Theora both depend on some initial header packets
  for decoder setup and initialization. We retrieve these first
  before entering the main decode loop. */

  /* Only interested in Theora streams */
  while (!stateflag)
  {
    int ret = buffer_data();
    if (ret == 0)
      break;

    while (ogg_sync_pageout(&iOggSyncState,&iOggPage)>0)
    {
      ogg_stream_state test;

      /* is this a mandated initial header? If not, stop parsing */
      if (!ogg_page_bos(&iOggPage))
      {
        /* don't leak the page; get it into the appropriate stream */
        queue_page(&iOggPage);
        stateflag=1;
        break;
      }

      ogg_stream_init(&test,ogg_page_serialno(&iOggPage));
      ogg_stream_pagein(&test,&iOggPage);
      ogg_stream_packetout(&test,&iOggPacket);

      /* identify the codec: try theora */
      if (!iTheoraPacketsCount && theora_decode_header(&iTheoraInfo, &iTheoraComment, &iOggPacket)>=0)
      {
        /* it is theora -- save this stream state */
        memcpy(&iOggStreamState,&test,sizeof(test));
        iTheoraPacketsCount = 1;
      }
      else
      {
        /* whatever it is, we don't care about it */
        ogg_stream_clear(&test);
      }
    }
    /* fall through to non-initial page parsing */
  }

  /* we're expecting more header packets. */
  while (iTheoraPacketsCount && iTheoraPacketsCount<3)
  {
    int ret;

    /* look for further theora headers */
    while (iTheoraPacketsCount && (iTheoraPacketsCount < 3))
    {
      ret = ogg_stream_packetout(&iOggStreamState, &iOggPacket);
      if (ret < 0)
      {
        iIrrLog->log("CTheoraPlayer: Error parsing Theora stream headers; corrupt stream?\n");
        return false;
      }
      if (theora_decode_header(&iTheoraInfo, &iTheoraComment, &iOggPacket))
      {
        iIrrLog->log("CTheoraPlayer: Error parsing Theora stream headers; corrupt stream?\n");
        return false;
      }
      iTheoraPacketsCount++;
    }


    /* The header pages/packets will arrive before anything else we
    care about, or the stream is not obeying spec */
    if(ogg_sync_pageout(&iOggSyncState, &iOggPage)>0)
    {
      queue_page(&iOggPage); /* demux into the stream state */
    }
    else
    {
      int ret=buffer_data(); /* need more data */
      if (ret == 0)
      {
        iIrrLog->log("CTheoraPlayer: End of file while searching for codec headers.\n");
        return false;
      }
    }
  }

  /* Now we have all the required headers. initialize the decoder. */
  if (iTheoraPacketsCount)
  {
    theora_decode_init(&iTheoraDecoderState, &iTheoraInfo);
    static c8 buffer[512] = {0};
    snprintf(buffer, 511, "Ogg logical stream %x is Theora %dx%d %.02f fps video\nEncoded frame content is %dx%d with %dx%d offset\n",
      (unsigned int)iOggStreamState.serialno,iTheoraInfo.width,iTheoraInfo.height,
      (double)iTheoraInfo.fps_numerator/iTheoraInfo.fps_denominator,
      iTheoraInfo.frame_width, iTheoraInfo.frame_height, iTheoraInfo.offset_x, iTheoraInfo.offset_y);
    iIrrLog->log(buffer);
  }
  else
  {
    /* tear down the partial theora setup */
    theora_info_clear(&iTheoraInfo);
    theora_comment_clear(&iTheoraComment);
  }

  /* queue any remaining pages from data we buffered but that did not
  contain headers */
  while (ogg_sync_pageout(&iOggSyncState, &iOggPage) > 0)
  {
    queue_page(&iOggPage);
  }

  return true;
}

//------------------------------------------------------------------------------
//! Create texture if needed and buffer for it.
//! If texture output is on we create buffer with the same parameters
//! to use memcpy function in synchronization section
bool CTheoraPlayer::PrepareBuffers()
{
  // create texture
  // unset auto mipmaps flag
  bool oldMipmapFlag = iIrrVideoDriver->getTextureCreationFlag(
    video::ETCF_CREATE_MIP_MAPS);
  iIrrDevice->getVideoDriver()->setTextureCreationFlag(
    video::ETCF_CREATE_MIP_MAPS, false);

  // create texture
  core::dimension2d<s32> size(iTheoraInfo.frame_width, iTheoraInfo.frame_height);
  iTexture = iIrrVideoDriver->addTexture(
    size, "CTheoraTexture", video::ECF_R8G8B8);

  // restore auto mipmaps flag
  iIrrVideoDriver->setTextureCreationFlag(
    video::ETCF_CREATE_MIP_MAPS, oldMipmapFlag);

  if (iTexture == NULL)
  {
    iIrrLog->log("CTheoraPlayer: Failed to create texture\n", ELL_ERROR);
    return false;
  }
  iTexture->grab();

  // we should care what buffer and texture
  // have the same size so we can use memcpy operation
  // create buffer image
  video::ECOLOR_FORMAT colorFormat = iTexture->getColorFormat();
  core::dimension2d<s32> imageSize = iTexture->getSize();
  u32 pitch = iTexture->getPitch();
  u32 height = imageSize.Height;

  u32 bufferSize = pitch*height;
  void* textureBuffer = (void*) new u8[bufferSize];
  if (textureBuffer == NULL)
  {
    iIrrLog->log("CTheoraPlayer: Failed to allocate buffer memory\n", ELL_ERROR);
    return false;
  }

  // create IImage, i couldn't get working parameters for createImageFromData()
  // to use textureBuffer as Data in CImage class and correctly delete it
  iImage = iIrrVideoDriver->createImageFromData(
    colorFormat, imageSize, textureBuffer);
  delete [] textureBuffer;
  if (iImage == NULL)
  {
    iIrrLog->log("CTheoraPlayer: Failed to create image\n", ELL_ERROR);
    return false;
  }

  return true;
}

//------------------------------------------------------------------------------
//! OnUpdate
//! Get next frame and update texture
bool CTheoraPlayer::OnUpdate(u32 timeMs)
{
  if (iState != CTPS_PLAYING)
    return true;

  //timeMs = 1000;

  // calculate how many frames need to be decoded
  iCurrTime += timeMs;
  u32 neededFrame = (u32)(1.0f*(iCurrTime/1000.0f)*iTheoraInfo.fps_numerator/iTheoraInfo.fps_denominator);
  u32 framesToDo = neededFrame - iCurrFrame;

  // now decode
  u32 i, wasLastFrameDecoded = false;
  for (i = 0; i < framesToDo; i++)
  {
    wasLastFrameDecoded = ProcessNextFrame();
    if (iState != CTPS_PLAYING)
      break;
  };

  if (wasLastFrameDecoded)
  {
    /* dumpvideo frame */
    UpdateBuffer();
    UpdateTexture();
  }

  return true;
}

//------------------------------------------------------------------------------
//! ProcessNextFrame
//! Decode next frame from ogg input stream.
//! Returns true if frame was successfully decoded
bool CTheoraPlayer::ProcessNextFrame()
{
  /*
  It's one Theora packet per frame, so this is pretty
  straightforward if we're not trying to maintain sync
  with other multiplexed streams.

  the videobuf_ready flag is used to maintain the input
  buffer in the libogg stream state. If there's no output
  frame available at the end of the decode step, we must
  need more input data. We could simplify this by just
  using the return code on ogg_page_packetout(), but the
  flag system extends easily to the case were you care
  about more than one multiplexed stream (like with audio
  playback). In that case, just maintain a flag for each
  decoder you care about, and pull data when any one of
  them stalls.

  videobuf_time holds the presentation time of the currently
  buffered video frame. We ignore this value.
  */

  /* single frame video buffering */
  int          videobuf_ready=0;
  ogg_int64_t  videobuf_granulepos=-1;
  double       videobuf_time=0;

  while (!videobuf_ready)
  {
    /* theora is one in, one out... */
    if (ogg_stream_packetout(&iOggStreamState, &iOggPacket)>0)
    {
      theora_decode_packetin(&iTheoraDecoderState, &iOggPacket);
      videobuf_granulepos=iTheoraDecoderState.granulepos;
      videobuf_time=theora_granule_time(&iTheoraDecoderState, videobuf_granulepos);
      videobuf_ready=1;
      iCurrFrame++;
    }

    // TODO: handle end of file
    if (!videobuf_ready && (iVideoFile->getPos() == iVideoFile->getSize()))
    {
      if (iIsLooped)
      {
        PrepareOgg();
      }
      else
      {
        Stop();
        return false;
      }
    }

    if (!videobuf_ready)
    {
      /* no data yet for somebody.  Grab another page */
      buffer_data();
      while(ogg_sync_pageout(&iOggSyncState, &iOggPage)>0){
        queue_page(&iOggPage);
      }
    }
  }

  return videobuf_ready != 0;
}


//------------------------------------------------------------------------------
//! Stop
//!
video::ITexture* CTheoraPlayer::GetTexture()
{
  if (iTexture != NULL)
  {
    return iTexture;
  }

  return iBlankTexture;
}

//------------------------------------------------------------------------------
//! Stop
//!
video::IImage* CTheoraPlayer::GetImage()
{
  return iImage;
}

//------------------------------------------------------------------------------
//! UpdateTexture
//! Copy buffer data to texture
void CTheoraPlayer::UpdateTexture()
{
  if (iImage == NULL)
    return;

  u32 texturePitch = iTexture->getPitch();
  const core::dimension2d<s32>& textureSize = iTexture->getSize();

  // try to lock texture
  u8* textureData = (u8*) iTexture->lock();
  if (textureData == NULL)
    return;

  iImage->copyToScaling(
    textureData,
    textureSize.Width,
    textureSize.Height,
    iTexture->getColorFormat(),
    texturePitch);

  iTexture->unlock();
}


//------------------------------------------------------------------------------
//! UpdateBuffer
//! Write out the planar YUV frame, uncropped
void CTheoraPlayer::UpdateBuffer()
{
  if (iImage == NULL)
    return;

  //gTimer.Start();

  yuv_buffer yuv;
  theora_decode_YUVout(&iTheoraDecoderState, &yuv);

  const core::dimension2d<s32>& imageSize = iImage->getDimension();
  _IRR_DEBUG_BREAK_IF(imageSize.Height < yuv.y_height ||
    imageSize.Width < yuv.y_width);


  u8* bufferData = (u8*)iImage->lock();
  u8* yData = yuv.y;
  u8* uData = yuv.u;
  u8* vData = yuv.v;

  int y, x;

  u32 rowSize = iImage->getPitch();
  switch (iImage->getColorFormat())
  {
    case video::ECF_A8R8G8B8:
    {
      for (y = 0; y < yuv.y_height; y++)
      {
        int xsize = yuv.y_width;
        int uvy = (y/2)*yuv.uv_stride;
        int yy = y*yuv.y_stride;
        int by = y*rowSize;
        for (x = 0; x < xsize; x++)
        {
          int Y = yuv.y[yy + x] - 16;
          int U = yuv.u[uvy + (x/2)] - 128;
          int V = yuv.v[uvy + (x/2)] - 128;
          int R = ((298*Y         + 409*V + 128)>>8);
          int G = ((298*Y - 100*U - 208*V + 128)>>8);
          int B = ((298*Y + 516*U         + 128)>>8);
          if (R<0) R=0; if (R>255) R=255;
          if (G<0) G=0; if (G>255) G=255;
          if (B<0) B=0; if (B>255) B=255;

          bufferData[by + x*4 + 0] = B;
          bufferData[by + x*4 + 1] = G;
          bufferData[by + x*4 + 2] = R;
          bufferData[by + x*4 + 3] = 0xFF;
        };
      };
    } break;

    case video::ECF_R8G8B8:
    {
      for (y = 0; y < yuv.y_height; y++)
      {
        int xsize = yuv.y_width;
        int uvy = (y/2)*yuv.uv_stride;
        int yy = y*yuv.y_stride;
        int by = y*rowSize;
        for (x = 0; x < xsize; x++)
        {
          int Y = yuv.y[yy + x] - 16;
          int U = yuv.u[uvy + (x/2)] - 128;
          int V = yuv.v[uvy + (x/2)] - 128;
          int R = ((298*Y         + 409*V + 128)>>8);
          int G = ((298*Y - 100*U - 208*V + 128)>>8);
          int B = ((298*Y + 516*U         + 128)>>8);
          if (R<0) R=0; if (R>255) R=255;
          if (G<0) G=0; if (G>255) G=255;
          if (B<0) B=0; if (B>255) B=255;

          bufferData[by + x*3 + 0] = B;
          bufferData[by + x*3 + 1] = G;
          bufferData[by + x*3 + 2] = R;
        };
      }
    } break;
  }

  //gTimer.Stop();

  //float timeDiff = gTimer.GetMS();
  //printf("UpdateBuffer: %.2f\n", timeDiff);
}

//------------------------------------------------------------------------------
//! queue_page
//! helper: push a page into the steam for packetization
irr::s32 CTheoraPlayer::queue_page(ogg_page *page)
{
  if (iTheoraPacketsCount)
    ogg_stream_pagein(&iOggStreamState, &iOggPage);
  return 0;
}

//------------------------------------------------------------------------------
//! buffer_data
//! Helper; just grab some more compressed bitstream and sync it for
//! page extraction
irr::s32 CTheoraPlayer::buffer_data()
{
  char* buffer = ogg_sync_buffer(&iOggSyncState, 4096);
  irr::s32 bytes = iVideoFile->read(buffer, 4096);
  ogg_sync_wrote(&iOggSyncState, bytes);
  return (bytes);
}
